Refactoring PostcodeApp - DI toepassen
We hebben drie TryOut methoden moeten maken voor elk van de data storage mogelijkheden (csv, xml en json).
We willen de code nu 'refactoren' en slechts één TryOut methode overhouden.
De techniek die we hiervoor gaan gebruiken is Dependency Injection (Inversion of Control en Dependency Injection in .NET Core).
Video
Stappenplan Dependency Injection
- Installeer het
Microsoft.Extensions.DependencyInjection
pakket via NuGet Package Console Zie Microsoft.Extensions.DependencyInjection op NuGet om te leren hoe je dat moet doen. - Om te verifiëren dat het pakket geïnstalleerd is open je het PostCodeApp.csproj bestand door met de rechermuisknop te klikken op de projectnaam in de verkennen en Edit PostcodeApp.csproj te kiezen uit de lijst:
- De service-provider
Als voorbeeld voor de registratie van een service gebruiken we de interface die we in Realisatiefase PostcodeApp gemaakt hebben.- De naam van die service is
IPostcode
en die staat in het bestand /Dal/IPostcode.cs.namespace PostcodeApp.Dal { public interface IPostcode { // Property signatures: // Een Postcode BLL object om de opgehaalde waarden // in op te slagen Bll.Postcode Postcode { get; set; } // Error message string Message { get; set; } string ConnectionString { get; set; } public char Separator { get; set; } // method signatures bool Create(); bool Create(char separator); bool ReadAll(); } }
- De drie klassen om csv, json en xml bestanden in te lezen zijn van deze signatuur. Let erop dat alle eigenschappen en methoden van de interface in deze drie methoden geïmplementeerd zijn. Ik plaats hier nogmaals de broncode:
- PostcodeCsv
using System; using System.Collections.Generic; using System.IO; namespace PostcodeApp.Dal { class PostcodeCsv : IPostcode { // Een Postcode BLL object om de opgehaalde waarden // in op te slagen public Bll.Postcode Postcode { get; set; } // Error message public string Message { get; set; } private string connectionString = @"Data/Postcode"; public string ConnectionString { get { return connectionString + ".csv"; } set { connectionString = value; } } public char Separator { get; set; } = ';'; public PostcodeCsv(Bll.Postcode postcode) { Postcode = postcode; } public bool ReadAll() { Helpers.Tekstbestand bestand = new Helpers.Tekstbestand(); bestand.FileName = ConnectionString; if (bestand.Lees()) { string[] postcodes = bestand.Text.Split('\n'); try { List<Bll.Postcode> list = new List<Bll.Postcode>(); foreach (string s in postcodes) { if (s.Length > 0) { list.Add(ToObject(s)); } } Postcode.List = list; Message = $"Het bestand {ConnectionString} is gedesialiseerd!"; return true; } catch (Exception e) { // Melding aan de gebruiker dat iets verkeerd gelopen is. // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie Message = $"Bestand {ConnectionString} is niet gedeserialiseerd.\nFoutmelding {e.Message}."; return false; } } else { Message = $"Bestand {ConnectionString} is niet gedeserialiseerd.\nFoutmelding {bestand.Melding}."; return false; } } private Bll.Postcode ToObject(string line) { Bll.Postcode postcode = new Bll.Postcode(); string[] values = line.Split(Separator); postcode.Code = values[0]; postcode.Plaats = values[1]; postcode.Provincie = values[2]; postcode.Localite = values[3]; postcode.Province = values[4]; return postcode; } /// <summary> /// De Create van CRUD /// In het geval van een CSV bestand wordt de hele List gecreëerd. /// </summary> /// <returns></returns> public bool Create() { try { TextWriter writer = new StreamWriter(ConnectionString); foreach (Bll.Postcode item in Postcode.List) { // One of the most versatile and useful additions to the C# language in version 6 // is the null conditional operator ?.Post writer.WriteLine("{0}{5}{1}{5}{2}{5}{3}{5}{4}", item?.Code, item?.Plaats, item?.Provincie, item?.Localite, item?.Province, Separator); } writer.Close(); Message = $"Het bestand met de naam {ConnectionString} is gemaakt!"; return true; } catch (Exception e) { // Melding aan de gebruiker dat iets verkeerd gelopen is. // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie Message = $"Bestand met naam {ConnectionString} niet gemaakt.\nFoutmelding {e.Message}."; return false; } } // Een overload om tegelijkertijd de separator in te stellen public bool Create(char separator = ';') { Separator = separator; return Create(); } } }
- PostcodeJson
using System; using System.Collections.Generic; using System.IO; namespace PostcodeApp.Dal { class PostcodeJson : IPostcode { // Een Postcode BLL object om de opgehaalde waarden // in op te slagen public Bll.Postcode Postcode { get; set; } // Error message public string Message { get; set; } private string connectionString = @"Data/Postcode"; public string ConnectionString { get { return connectionString + ".json"; } set { connectionString = value; } } public char Separator { get; set; } = ';'; public PostcodeJson(Bll.Postcode postcode) { Postcode = postcode; } // een overload om de naam van het csv bestand in te stellen public PostcodeJson(string connectionString) { ConnectionString = connectionString; } /// <summary> /// In het geval van JSON wordt heel de List gesaved /// </summary> public bool Create() { try { TextWriter writer = new StreamWriter(ConnectionString); // static method SerilizeObject van Newtonsoft.Json string postcodeString = Newtonsoft.Json.JsonConvert.SerializeObject(Postcode.List); writer.WriteLine(postcodeString); writer.Close(); Message = $"Het bestand met de naam {ConnectionString} is met succes geserialiseerd."; return true; } catch (Exception e) { // Melding aan de gebruiker dat iets verkeerd gelopen is. // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie Message = $"Kan het bestand met de naam {ConnectionString} niet serialiseren.\nFoutmelding {e.Message}."; return false; } } public bool Create(char separator = ';') { Separator = separator; return Create(); } public bool ReadAll() { try { Helpers.Tekstbestand bestand = new Helpers.Tekstbestand(); bestand.FileName = ConnectionString; bestand.Lees(); Postcode.List = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Bll.Postcode>>(bestand.Text); Message = $"Het bestand met de naam {ConnectionString} is met succes geserialiseerd."; return true; } catch (Exception e) { // Melding aan de gebruiker dat iets verkeerd gelopen is. // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie Message = $"Kan het bestand met de naam {ConnectionString} niet deserialiseren.\nFoutmelding {e.Message}."; return false; } } } }
- PostcodeXml
using System; using System.Collections.Generic; using System.IO; using System.Xml.Serialization; namespace PostcodeApp.Dal { class PostcodeXml : IPostcode { // Een Postcode BLL object om de opgehaalde waarden // in op te slagen public Bll.Postcode Postcode { get; set; } // Error message public string Message { get; set; } private string connectionString = @"Data/Postcode"; public string ConnectionString { get { return connectionString + ".xml"; } set { connectionString = value; } } public char Separator { get; set; } = ';'; public PostcodeXml() { } public PostcodeXml(Bll.Postcode postcode) { Postcode = postcode; } // een overload om de naam van het csv bestand in te stellen public PostcodeXml(string connectionString) { ConnectionString = connectionString; } /// <summary> /// In het geval van XML wordt heel de List gesaved. /// </summary> /// <returns></returns> public bool Create() { try { XmlSerializer serializer = new XmlSerializer(typeof(Bll.Postcode[])); TextWriter writer = new StreamWriter(ConnectionString); //De serializer werkt niet voor een generieke lijst en ook niet voor ArrayList // dus eerst omzetten naar array Bll.Postcode[] postcodes = Postcode.List.ToArray(); serializer.Serialize(writer, postcodes); writer.Close(); Message = $"Bestand {ConnectionString} is met succes geserialiseerd."; return true; } catch (Exception e) { // Melding aan de gebruiker dat iets verkeerd gelopen is. // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie Message = $"Bestand {ConnectionString} is niet geserialiseerd.\nFoutmelding {e.Message}."; return false; } } public bool Create(char separator = ';') { Separator = separator; return Create(); } public bool ReadAll() { try { XmlSerializer serializer = new XmlSerializer(typeof(Bll.Postcode[])); StreamReader file = new System.IO.StreamReader(ConnectionString); Bll.Postcode[] postcodes = (Bll.Postcode[])serializer.Deserialize(file); file.Close(); // array converteren naar List Postcode.List = new List<Bll.Postcode>(postcodes); Message = $"Bestand {ConnectionString} is met succes gedeserialiseerd."; return true; } catch (Exception e) { // Melding aan de gebruiker dat iets verkeerd gelopen is. // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie Message = $"Het bestand {ConnectionString} s niet gedeserialiseerd.\nFoutmelding {e.Message}."; return false; } } } }
- PostcodeCsv
- De naam van die service is
-
Registratie van een service
-
Nu moeten we onze dependency injection container installeren. Pas dan beschikken we over een manier om de afzonderlijke componenten te registreren die we in ons programma zullen gebruiken.
Deze wordt geleverd door de klasse
ServiceCollection
. We maken een instantie van deServiceCollection
-klasse en voegen services toe aan deze klasse.Voordat we kunnen praten over hoe injectie in de praktijk wordt gedaan, is het cruciaal om te begrijpen wat de levensduur van de service is. Wanneer een component een andere component inroept via dependency inejction kan je bepalen of de instantie die de vragende component terugkrijgt uniek is voor die compenent of niet, m.a.w. je kan de levensduur van de opgevraagde component bepalen. Het levensduur bepaalt dus hoe vaak een component wordt geInstantieerd, en of een component wordt gedeeld of niet.
-
De ingebouwde DI-container in .NET Core heeft drie opties:
Applicatie-brede configuratiecontainers registreer je als
Singleton
. Database toegangsklassen zoalsDbContext
van Entity Framework registreer je best alsScoped
, zodat de verbinding opnieuw kan worden gebruikt. Als je iets parallel wilt uitvoeren is het beter om dat te registreren alsTransient
. Dan krijgt elke component zijn eigen instantie en kunnen zij parallel lopen.Ik registreer het bestandstype als een concrete implementatie van de
IPostcode
-interface met behulp van een singleton-scope. Door dit te doen, probeer ik deprogram
klasse te gebruiken als een shell die de servicecontainer maakt en de benodigde services registreert.- Singleton: betekent dat er maar een enkele instantie ooit zal worden gemaakt. Deze instantie wordt gedeeld tussen alle componenten die het nodig hebben. Het is dezelfde instantie die altijd opnieuw wordt gebruikt.
- Scoped: betekent er per scope één instantie wordt gemaakt. Er wordt een scope bij elke request aan de applicatie gecreëerd, waardoor alle componenten die geregistreerd zijn als Scoped, één keer per request worden gecreëerd.
- Transient (vergankelijk) componenten worden gemaakt telkens ze worden aangevraagd en ze worden nooit gedeeld.
- Registratie van de Postcode service-provider
We maken een methode in de Program klasse met de naamConfigureServices():
private static void ConfigureServices(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<PostcodeApp.Dal.IPostcode> (p => new PostcodeApp.Dal.PostcodeJson(new PostcodeApp.Bll.Postcode())); }
-
- Gebruik maken van een service
- we roepen deze methode op in de
Main
van deProgram
klasse; - we configureren de services;
- we builden de ServiceProvider;
- we halen de service-provider op die we nodig hebben;
- en tenslote voeren we de generieke
TryOut
methode uit;static void Main(string[] args) { Console.WriteLine("Hello World!"); ServiceCollection serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); var service = serviceProvider.GetService<IPostcode>(); TryOut(service); }
- De
TryOut
methodestatic void TryOut(Dal.IPostcode dal) { Console.WriteLine("De Postcode App"); // de seperator staat standaard op ; // in het Postcode.csv bestand is dat | dal.Separator = ';'; dal.ReadAll(); Console.WriteLine(dal.Message); View.PostcodeConsole view = new View.PostcodeConsole(dal.Postcode); view.List(); // serialize postcodes met een andere separator // naar ander bestand dal.ConnectionString = "Data/Postcode2"; dal.Create(';'); Console.WriteLine(dal.Message); }
- Als we beslissen om een andere provider te gebruiken volstaat het die in de dependency-container te injecteren:
private static void ConfigureServices(IServiceCollection serviceCollection) { // add services serviceCollection.AddSingleton<Dal.IPostcode>(p => new Dal.PostcodeJson(new Bll.Postcode())); // add app serviceCollection.AddTransient<App>(); }
Zoals je kan zien is deze manier van werken eenvoudiger dan de verschillende TryOut methoden uit Realisatiefase PostcodeApp. We hebben nu slechts één tryout methode die we netjes in een App klasse hebben ondergebracht.
- we roepen deze methode op in de
- Om de scheiding (seperation of concern) tussen onze bedrijfsgebaseerde logica en de logica die we gebruiken om de werkelijke console-applicatie te configureren te behouden, maken we een nieuwe klasse met de naam App.cs. Het is de bedoeling om Program.cs te gebruiken om alles wat onze app nodig heeft op te starten, zoals de services die nodig zijn voor onze business logica.
- De eigenlijke businesslogica, die nu staat in de methode TryOut van de Program klasse verplaatsen we naar App.cs.
using System; namespace PostcodeApp { public class App { private readonly Dal.IPostcode dal; public App(Dal.IPostcode postcodeDal) { this.dal = postcodeDal; } public void TryOut() { Console.WriteLine("De Postcode App"); // de seperator staat standaard op ; // in het Postcode.csv bestand is dat | dal.Separator = ';'; dal.ReadAll(); Console.WriteLine(dal.Message); View.PostcodeConsole view = new View.PostcodeConsole(dal.Postcode); view.List(); // serialize postcodes met een andere separator // naar ander bestand dal.ConnectionString = "Data/Postcode2"; dal.Create(';'); Console.WriteLine(dal.Message); } } }
- App.cs heeft een object nodig dat voldoet aan het
Dal/IPostcode
-interfacecontract. Dit object wordt door onze dependency manager doorgegeven. Open het Program.cs bestand en voeg het volgende toe:using Microsoft.Extensions.DependencyInjection; using PostcodeApp.Dal; using System; namespace PostcodeApp { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); ServiceCollection serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); serviceProvider.GetService<App>().TryOut(); } private static void ConfigureServices(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<PostcodeApp.Dal.IPostcode> (p => new PostcodeApp.Dal.PostcodeJson(new PostcodeApp.Bll.Postcode())); // add app serviceCollection.AddTransient<App>(); } } }
Onze console app start inMain
. Hier creëren we een nieuwServiceCollection
-object en configureren het in deConfigureServices
methode. InConfigureServices
voegen we onze dependancies toe aan de containercollectie. Die kunnen een levensduur vanScoped
,Transient
ofSingleton
hebben.
Zodra het objectServiceCollection
is geconfigureerd, moeten we eenIServiceProvider
(Dependency Management Container) opvragen uit onsServiceCollection
-object waarmee we handmatig onze App-klasse handmatig instantiëren die de businesslogica van onze app uitvoert door middel van de methodeRun
.
- De eigenlijke businesslogica, die nu staat in de methode TryOut van de Program klasse verplaatsen we naar App.cs.
JI